﻿#region MIT Licence
/*
 * Copyright (C) 2014 PdfCreative
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software 
 * and associated documentation files (the "Software"), to deal in the Software without restriction, 
 * including without limitation the rights to use, copy, modify, merge, publish, distribute, 
 * sublicense, and/or sell copies of the Software, and to permit persons to whom 
 * the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included 
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE 
 * AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
#endregion

using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Text;

namespace PdfCreative.Pdf.Graphics
{
    /// <summary>
    /// The Matrix class represents a matrix of numbers. It is used to store coordinate transformations.
    /// It contains a number of methods for common transformations such as scaling, rotating, skewing and translating,
    /// and arbitrary matrix multiplications.
    /// </summary>
    [Serializable]
    public class Matrix : ICloneable, ISerializable
    {
        // Private fields
        // ==============
        #region Private fields
        private float[,] _matrix;
        #endregion

        

        // Public properties
        // =================
        #region Public properties
        /// <summary>
        /// Sets or retrieves the value at the specified column and row.
        /// </summary>
        /// <param name="column">The column</param>
        /// <param name="row">The row</param>
        /// <returns>The value at that point inside the matrix.</returns>
        public float this[int column, int row]
        {
            get { return _matrix[column, row]; }
            set { _matrix[column, row] = value; }
        }

        /// <summary>
        /// Retrieves the number of columns in the matrix.
        /// </summary>
        public int Columns
        {
            get { return _matrix.GetLength(0); }
        }

        /// <summary>
        /// Retrieves the number of columns in the matrix.
        /// </summary>
        public int Rows
        {
            get { return _matrix.GetLength(1); }
        }
        #endregion



        // Public methods
        // ==============
        #region Public methods
        /// <summary>
        /// Adds the contents of the specified matrix to this matrix.
        /// If the number of rows of columns in the second matrix exceeds that of this matrix, the contents will be truncated.
        /// </summary>
        /// <param name="other">The matrix to add the contents of</param>
        public void Add(Matrix other)
        {
            for(int rowEnum=0; rowEnum<Math.Min(this.Rows, other.Rows); rowEnum++)
                for(int colEnum=0; colEnum<Math.Min(this.Columns, other.Columns); colEnum++)
                    this._matrix[colEnum, rowEnum] = this._matrix[colEnum, rowEnum] + other._matrix[colEnum, rowEnum];
        }

        /// <summary>
        /// Compares the values in this matrix with those in the specified other matrix, and returns the
        /// result. The matrices are considered to have the same values if and only if their sizes are
        /// the same, and every value in the one matrix is equal to the corresponding value in the other.
        /// </summary>
        /// <param name="other">The other matrix</param>
        /// <returns>True if all the values are the same; False if one or more values are different.</returns>
        public bool CompareValues(Matrix other)
        {
            if(null==other)
                throw new ArgumentNullException("other");

            if (this.Rows != other.Rows || this.Columns != other.Columns)
                return false;

            for (int rowEnum = 0; rowEnum < this.Rows; rowEnum++)
                for (int colEnum = 0; colEnum < this.Columns; colEnum++)
                    if (this._matrix[colEnum, rowEnum] != other._matrix[colEnum, rowEnum])
                        return false;

            return true;
        }

        /// <summary>
        /// Multiplies the current matrix with the specified matrix and returns the result. 
        /// Matrix multiplication only works if the number of rows of the second matrix mathes the number of columns of this matrix.
        /// The result is a matrix whose dimensions are determined by the number of rows in this matrix, and the number of columns in
        /// the other matrix.
        /// </summary>
        /// <param name="other">The matrix to multiply this matrix with</param>
        public Matrix Multiply(Matrix other)
        {
            if (this.Columns != other.Rows)
                throw new InvalidOperationException("The number of columns in this matrix must match the number of rows in the specified matrix.");

            Matrix result = new Matrix(other.Columns, this.Rows);
            
            int elements = this.Columns;
            for (int rowEnum = 0; rowEnum < result.Rows; rowEnum++)
                for (int colEnum = 0; colEnum < result.Columns; colEnum++)
                    for (int elementEnum = 0; elementEnum < elements; elementEnum++)
                        result._matrix[colEnum, rowEnum] = result._matrix[colEnum, rowEnum] + (this._matrix[elementEnum, rowEnum] * other._matrix[colEnum, elementEnum]);
            
            return result;
        } // Multiply()

        /// <summary>
        /// Multiplies the current matrix by the specified vector, and returns the result.
        /// The vector must be of a dimension equal to the number of columns in the matrix.
        /// </summary>
        /// <param name="vector">The vector to multiply this matrix by</param>
        /// <returns>The result vector</returns>
        public Vector Multiply(Vector vector)
        {
            if(vector.Length != this.Columns)
                throw new InvalidOperationException("The length of the specified vector must match the number of rows in the current matrix");

            Vector result = new Vector(this.Rows);

            for (int rowEnum = 0; rowEnum < this.Rows; rowEnum++)
                for (int colEnum = 0; colEnum < this.Columns; colEnum++)
                    result[rowEnum] = result[rowEnum] + (_matrix[colEnum, rowEnum]*vector[colEnum]);

            return result;
        }

        /// <summary>
        /// Sets up the matrix such that it becomes an identity matrix.
        /// If the number of columns is not equal to the number of rows, the result will not be a perfect identity matrix.
        /// </summary>
        public void SetIdentityValues()
        {
            for(int rowEnum=0; rowEnum < this.Rows; rowEnum++)
                for(int colEnum=0; colEnum < this.Columns; colEnum++)
                    _matrix[colEnum, rowEnum] = (colEnum == rowEnum ? 1  : 0);
        }

        /// <summary>
        /// Copies the specified values into the matrix.
        /// </summary>
        /// <param name="values">The values</param>
        public void SetValues(params float[] values)
        {
            if (null == values) throw new ArgumentNullException("values");

            int rowEnum = 0;
            int colEnum = 0;

            // Go through all the values in the list, and insert them into the matrix in the right place.
            foreach(float value in values)
            {
                // Insert into the matrix
                _matrix[colEnum, rowEnum] = value;

                // Next column. If this was the last column, we go down a row, and start at the first column again.
                colEnum++;
                if (colEnum >= this.Columns)
                {
                    colEnum = 0; rowEnum++;
                    if (rowEnum >= this.Rows) break;
                }
            }
        }

        /// <summary>
        /// Performs a coordinate transformation by multiplying a rotation matrix by the current matrix instance.
        /// The current matrix instance will be set to the result of the multiplication.
        /// Because the rotation matrix has dimensions (3,3) the current instance must have 3 rows. In order for the rotation to
        /// make sense, the current instance must have dimensions (3,3).
        /// </summary>
        /// <param name="radians">The rotation angle, in radians anti-clockwise.</param>
        public void TransformRotate(float radians)
        {
            // Set up the rotation matrix
            Matrix rotation = new Matrix(
                3, 
                3, 
                (float)Math.Cos(radians), 
                -(float)Math.Sin(radians), 
                0, 
                (float)Math.Sin(radians), 
                (float)Math.Cos(radians), 
                0, 
                0, 
                0, 
                1f);
            
            // Multiply the rotation matrix with the current instance, and alter the current instance so that it matches the result.
            Matrix result = this.Multiply(rotation); //this);
            _matrix = result._matrix;
        } // TransformRotate()

        /// <summary>
        /// Performs a coordinate transformation by multiplying a scaling matrix by the current matrix instance.
        /// The current matrix instance will be set to the result of the multiplication.
        /// Because the scaling matrix has dimensions (3,3) the current instance must have 3 rows. In order for the rotation to
        /// make sense, the current instance must have dimensions (3,3).
        /// </summary>
        /// <param name="xscale">The scale for the X axis</param>
        /// <param name="yscale">The scale for the Y axis</param>
        public void TransformScale(float xscale, float yscale)
        {
            // Set up the scaling matrix
            Matrix scale = new Matrix(
                3,
                3,
                xscale,
                0,
                0,
                0,
                yscale,
                0,
                0,
                0,
                1f);

            // Multiply the scaling matrix with the current instance, and alter the current instance so that it matches the result.
            Matrix result = this.Multiply(scale); //this);
            _matrix = result._matrix;

        } // TransformScale()

        /// <summary>
        /// Performs a coordinate transformation by multiplying a skewing matrix by the current matrix instance.
        /// The current matrix instance will be set to the result of the multiplication.
        /// Because the skewing matrix has dimensions (3,3) the current instance must have 3 rows. In order for the rotation to
        /// make sense, the current instance must have dimensions (3,3).
        /// </summary>
        /// <param name="radiansX">The angle, in radians, to skew the X axis by</param>
        /// <param name="radiansY">The angle, in radians, to skew the Y axis by</param>
        public void TransformSkew(float radiansX, float radiansY)
        {
            // Set up the skew matrix
            Matrix skew= new Matrix(
                3,
                3,
                1,
                (float)Math.Tan(radiansY),
                0,
                (float)Math.Tan(radiansX),
                1,
                0,
                0,
                0,
                1f);

            // Multiply the skew matrix with the current instance, and alter the current instance so that it matches the result.
            Matrix result = this.Multiply(skew); //this);
            _matrix = result._matrix;

        } // TransformSkew()

        /// <summary>
        /// Performs a coordinate transformation by multiplying a translation matrix by the current matrix instance.
        /// The current matrix instance will be set to the result of the multiplication.
        /// Because the translation matrix has dimensions (3,3) the current instance must have 3 rows. In order for the rotation to
        /// make sense, the current instance must have dimensions (3,3).
        /// </summary>
        /// <param name="xoff">The amount to translate in the X direction</param>
        /// <param name="yoff">The amount to translate in the Y direction.</param>
        public void TransformTranslate(float xoff, float yoff)
        {
            // Set up the translation matrix
            Matrix translation = new Matrix(
                3,
                3,
                1,
                0,
                xoff,
                0,
                1,
                yoff,
                0,
                0,
                1f);

            // Multiply the translation matrix with the current instance, and alter the current instance so that it matches the result.
            Matrix result = this.Multiply(translation); //this);
            _matrix = result._matrix;

        } // TransformTranslate()

        #endregion



        // Base class overrides
        // ====================
        #region Object overrides
        /// <summary>
        /// Indicates if this matrix is equal to another instance.
        /// </summary>
        /// <param name="obj">The instance to compare this matrix to. This must be a non-null Matrix instance.</param>
        /// <returns>A boolean value indicating that the instances are equal (true) or not.</returns>
        public override bool Equals(object obj)
        {
            bool result = false;

            if (null != obj)
            {
                Matrix other = obj as Matrix;
                if (null != other)
                    result = this._matrix.Equals(other._matrix);

            }

            return result;
        } // Equals()

        /// <summary>
        /// Retrieves the hash code for this matrix. This calculates a hash code from the hash codes of all
        /// of its values; for large matrices this may be an expensive operation.
        /// </summary>
        /// <returns>The hash code.</returns>
        public override int GetHashCode()
        {
            int hashCode = 0;
            foreach (float val in _matrix)
                hashCode ^= val.GetHashCode();

            return hashCode;
        }

        /// <summary>
        /// Returns the string representation of the matrix
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();

            sb.AppendFormat("({0}x{1}) [", this.Columns, this.Rows);
            for (int rowEnum = 0; rowEnum < this.Rows; rowEnum++)
            {
                if (rowEnum > 0) sb.Append(" | ");
                for (int columnEnum = 0; columnEnum < this.Columns; columnEnum++)
                {
                    if (columnEnum > 0) sb.Append(' ', 3);
                    sb.AppendFormat("{0:F2}", _matrix[columnEnum, rowEnum]);
                }
            }

            sb.Append("]");

            return sb.ToString();
        } // ToString()
        #endregion



        // Interface implementations
        // =========================
        #region ICloneable interface
        /// <summary>
        /// Creates a clone of this matrix.
        /// </summary>
        /// <returns>The matrix</returns>
        public Object Clone()
        {
            return new Matrix(this);
        }

        #endregion

        #region ISerializable Members

        /// <summary>
        /// Serialises the instance into the specified serialisation stream.
        /// </summary>
        /// <param name="info">The serialisation stream.</param>
        /// <param name="context">The serialisation context.</param>
        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("columns", this.Columns);
            info.AddValue("rows", this.Rows);
            info.AddValue("matrix", _matrix);
        }

        #endregion



        // Public constructor
        // ==================
        #region Public constructor
        /// <summary>
        /// Constructs a new matrix of the specified size.
        /// </summary>
        /// <param name="columns">The number of columns in the matrix.</param>
        /// <param name="rows">The number of rows in the matrix.</param>
        public Matrix(int columns, int rows)
        {
            _matrix = new float[columns, rows];
        }

        /// <summary>
        /// Constructs a new matrix of the specified size, and fills it with the specified values.
        /// </summary>
        /// <param name="columns">The number of columns in the matrix.</param>
        /// <param name="rows">The number of rows in the matrix.</param>
        /// <param name="values">The values to initialise the matrix with.</param>
        public Matrix(int columns, int rows, params float[] values)
        {
            _matrix = new float[columns, rows];

            SetValues(values);
        }

        /// <summary>
        /// Constructs a new matrix that is a copy of the specified matrix.
        /// </summary>
        /// <param name="matrix">The matrix to base this matrix on</param>
        public Matrix(Matrix matrix)
        {
            _matrix = new float[matrix.Columns,matrix.Rows];
            this.Add(matrix);
        }

        /// <summary>
        /// Constructs a new matrix out of the specified serialisation stream.
        /// </summary>
        /// <param name="info">The serialisation stream.</param>
        /// <param name="context">The serialisation context.</param>
        public Matrix(SerializationInfo info, StreamingContext context)
        {
            info.GetInt32("columns");
            info.GetInt32("rows");
            _matrix = (float[,])info.GetValue("matrix", typeof(float[,]));
        }
        #endregion


    } // Matrix class
}
